//
// Created by sheyifan on 2021-05-11.
//
#include <vm.h>
#include <cstdio>
#include <ctime>
#include <unistd.h>
#include <csignal> // SIGINT

#ifdef _WIN32
#include <io/win/wininterpt.h>
#endif
#ifdef __linux__
#include <io/unix/unixinterpt.h>
#endif

bool LC3VM::checkPlatform() {
    if (sizeof(UINT16) != 2) {
        fprintf(stderr, "Error: size of instruction is not 16bits. Real size: %d.", sizeof(UINT16));
        return false;
    }

    return true;
}

// convert instruction between big endian and little endian
void LC3VM::swapEndian(UINT16& ins) {
    // revert high 8 bits and low 8 bits
    ins =  (ins << 8) | (ins >> 8);
}

/**
 * Extend signed number to 16 bits (remaining bits of the original must all zeros)
 * @param num signed number
 * @param bitCount original bit count of signed number
 */
UINT16 LC3VM::signExtend(UINT16 num, UINT16 bitCount) {
#ifdef DEBUG
    if (num >> bitCount) {
        fprintf(stderr, "Error: remaining bits not all zeros.\n");
        this->running = false;
    }
    else {
        // fprintf(stdout, "Success: remaining bits are all zeros.\n");
    }
#endif

    if ((num >> (bitCount - 1)) & 0x1) {
        // extend with 1
        num |= (0xFFFF << bitCount);
    }
    else {
        // No need to operate. remaining bits all zeros
    }

    return num;
}

void LC3VM::loadImage() {
    // 2021-05-14 Bug: mode "r" can cause incomplete reading for binary files
    FILE* imageFile = fopen(imagePath, "rb");
    if (!imageFile) {
        fprintf(stderr, "Error: fail to load image from %s.", imagePath);
        return;
    }

    fread(&origin, 2, 1, imageFile);
    swapEndian(origin);
    // 2021-05-14 Issue: initialize PC register
    regs[R_PC] = origin;

    this->insCount = fread(memory + origin, 2, UINT16_MAX - origin, imageFile);
    this->imageSize = insCount * 2;

    for (int i = 0 ; i < insCount ; i++) {
        swapEndian(memory[i + origin]);
    }

    fclose(imageFile);
}

LC3VM::LC3VM(const char* imagePath) {
    this->imagePath = imagePath;
    this->loadImage();
}

bool LC3VM::start() {
    clearLog();
    if (!checkPlatform()) {
        return false;
    }

    /* Setup */
    signal(SIGINT, handleInterrupt);
    disableInputBuffer();

    this->running = true;
    while (this->running) {
        UINT16 instr = this->fetch();
    #ifdef DEBUG
        char regLog[100];
        sprintf(regLog, "R0: 0x%.4x(%.5u)\tR1: 0x%.4x(%.5u)\tR2: 0x%.4x(%.5u)\tR3: 0x%.4x(%.5u)\nR4: 0x%.4x(%.5u)\tR5: 0x%.4x(%.5u)\tR6: 0x%.4x(%.5u)\tR7: 0x%.4x(%.5u)\nPC: 0x%.4x(%.5u)\tCD: 0x%.4x(%.5u)\t0x32D0: 0x%.4x\n\n",
                regs[R_R0], regs[R_R0],
                regs[R_R1], regs[R_R1],
                regs[R_R2], regs[R_R2],
                regs[R_R3], regs[R_R3],
                regs[R_R4], regs[R_R4],
                regs[R_R5], regs[R_R5],
                regs[R_R6], regs[R_R6],
                regs[R_R7], regs[R_R7],
                regs[R_PC], regs[R_PC],
                regs[R_COND], regs[R_COND], memory[0x32D0]);
        this->exportLog("./instr.log", "a+", regLog);
        time_t timer = time(nullptr);
        struct tm* tm_info = localtime(&timer);
        char ftime[100];
        strftime(ftime, 26, "Time: %Y-%m-%d %H:%M:%S", tm_info);
        char instrLogs[100];
        sprintf(instrLogs, "%s\tInstruction: 0x%.4x(%.5d)\tPC: 0x%.4x(%u)\n", ftime, instr, instr, this->regs[R_PC], this->regs[R_PC]);
        this->exportLog(R"(./instr.log)", "a+", instrLogs);
    #endif
        this->execute(instr);

        if (this->regs[R_PC] == this->getInsCount() + this->getOrigin()) {
            this->running = false;
            break;
        }
    }

    /* Shutdown */
    restoreInputBuffer();

    return true;
}

/**
 * @param pc program pointer
 * @return instruction
 */
UINT16 LC3VM::readMemory(UINT16 pc) {
    if (pc == MR_KBSR)
    {
        // printf("Check keyboard. PC: 0x%.4x\n", pc);
        if (checkKey())
        {
            memory[MR_KBSR] = (1 << 15);
            memory[MR_KBDR] = getchar();
        }
        else
        {
            memory[MR_KBSR] = 0;
        }
    }

    return memory[pc];
}

// Update conditional register on writing to register
void LC3VM::updateFlag(UINT16 reg) {
    if (regs[reg] == 0) {
        regs[R_COND] = FL_ZRO;
    }
    // Negative
    else if (regs[reg] >> 15) {
        regs[R_COND] = FL_NEG;
    }
    // Positive
    else {
        regs[R_COND] = FL_POS;
    }
}

// Get instruction from current pc, and then pc + 1 (before decode and execute instruction)
UINT16 LC3VM::fetch() {
    UINT16 instr = this->readMemory(this->regs[R_PC]);
    this->regs[R_PC]++;

    return instr;
}

void LC3VM::execute(UINT16 instr) {
    UINT16 op = instr >> 12;

    // for image "rogue"
//    if (this->regs[R_PC] == 0x30DC) {
//        return;
//    }

    switch (op) {
        case OP_ADD: {
        #ifdef DEBUG
            char addOpLog[100];
            sprintf(addOpLog, "Operator ADD: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", addOpLog);
        #endif
            ADD(instr);
            break;
        }
        case OP_AND: {
        #ifdef DEBUG
            char andOpLog[100];
            sprintf(andOpLog, "Operator AND: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", andOpLog);
        #endif
            AND(instr);
            break;
        }
        case OP_NOT: {
        #ifdef DEBUG
            char notOpLog[100];
            sprintf(notOpLog, "Operator NOT: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", notOpLog);
        #endif
            NOT(instr);
            break;
        }
        case OP_BR: {
        #ifdef DEBUG
            char brOpLog[100];
            sprintf(brOpLog, "Operator BR: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", brOpLog);
        #endif
            BR(instr);
            break;
        }
        case OP_JMP: {
        #ifdef DEBUG
            char jmpOpLog[100];
            sprintf(jmpOpLog, "Operator JMP: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", jmpOpLog);
        #endif
            JMP(instr);
            break;
        }
        case OP_JSR: {
        #ifdef DEBUG
            char jsrOpLog[100];
            sprintf(jsrOpLog, "Operator JSR: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", jsrOpLog);
        #endif
            JSR(instr);
            break;
        }
        case OP_LD: {
        #ifdef DEBUG
            char ldOpLog[100];
            sprintf(ldOpLog, "Operator LD: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", ldOpLog);
        #endif
            LD(instr);
            break;
        }
        case OP_LDI: {
        #ifdef DEBUG
            char ldiOpLog[100];
            sprintf(ldiOpLog, "Operator LDI: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", ldiOpLog);
        #endif
            LDI(instr);
            break;
        }
        case OP_LDR: {
        #ifdef DEBUG
            char ldrOpLog[100];
            sprintf(ldrOpLog, "Operator LDR: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", ldrOpLog);
        #endif
            LDR(instr);
            break;
        }
        case OP_LEA: {
        #ifdef DEBUG
            char leaOpLog[100];
            sprintf(leaOpLog, "Operator LEA: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", leaOpLog);
        #endif
            LEA(instr);
            break;
        }
        case OP_ST: {
        #ifdef DEBUG
            char stOpLog[100];
            sprintf(stOpLog, "Operator ST: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", stOpLog);
        #endif
            ST(instr);
            break;
        }
        case OP_STI: {
        #ifdef DEBUG
            char stiOpLog[100];
            sprintf(stiOpLog, "Operator STI: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", stiOpLog);
        #endif
            STI(instr);
            break;
        }
        case OP_STR: {
        #ifdef DEBUG
            char strOpLog[100];
            sprintf(strOpLog, "Operator STR: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", strOpLog);
        #endif
            STR(instr);
            break;
        }
        case OP_TRAP: {
        #ifdef DEBUG
            char trapOpLog[100];
            sprintf(trapOpLog, "Operator TRAP: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", trapOpLog);
        #endif
            switch (instr & 0xFF) {
                case TRAP_GETC: {
                    FGETC();
                    break;
                }
                case TRAP_OUT: {
                    FOUT();
                    break;
                }
                case TRAP_PUTS: {
                    FPUTS();
                    break;
                }
                case TRAP_IN: {
                    FIN();
                    break;
                }
                case TRAP_PUTSP: {
                    FPUTSP();
                    break;
                }
                case TRAP_HALT: {
                    FHALT();
                    break;
                }
                default: {
                    fprintf(stderr, "Error: unspecified trap routine.");
                    break;
                }
            }

            break;
        }
        case OP_RES: {
        #ifdef DEBUG
            char resOpLog[100];
            sprintf(resOpLog, "Operator RES: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", resOpLog);
        #endif
            break;
        }
        case OP_RTI: {
        #ifdef DEBUG
            char rtiOpLog[100];
            sprintf(rtiOpLog, "Operator RTI: 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", rtiOpLog);
        #endif
            break;
        }
        default: {
        #ifdef DEBUG
            char noOpLog[100];
            sprintf(noOpLog, "Error: unspecified operator 0x%.2x(%.2u)\n", op, op);
            exportLog("./instr.log", "a+", noOpLog);
        #endif
            this->running = false;
            fprintf(stderr, "Error: unspecified operator 0x%.2x(%.2u)\n", op, op);
            break;
        }
    }
#ifdef DEBUG
    exportLog("./instr.log", "a+", "\n");
#endif
}

void LC3VM::exportLog(const char* logPath, const char* mode, const char* log) {
    FILE* logFile = fopen(logPath, "a+");
    fprintf(logFile, log);
    fclose(logFile);
}

void LC3VM::clearLog() {
    if (access(R"(.\instr.log)", F_OK) == 0) {
        // File exists
        int del = remove(R"(.\instr.log)");
        if (!del)
            printf("Log file is Deleted successfully.\n");
        else
            printf("Log file is not Deleted.\n");
    } else{

    }
}

void LC3VM::ADD(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 SR1 = (instr >> 6) & 0x7;
    UINT16 flag = (instr >> 5) & 0x1;

    // Immediate mode
    if (flag) {
        UINT16 imm5 = signExtend(instr & 0x1F, 5);
        regs[DR] = regs[SR1] + imm5;
    #ifdef DEBUG
        char addLog[100];
        sprintf(addLog, "Mode: Immediate, DR: 0x%.1x(%.1u), SR: 0x%.1x(%.1u), imm5: %.2d\n", DR, DR, SR1, SR1, imm5);
        exportLog("./instr.log", "a+", addLog);
    #endif
    }
    // Register mode
    else {
        UINT16 SR2 = instr & 0x7;
        regs[DR] = regs[SR1] + regs[SR2];
    #ifdef DEBUG
        char addLog[100];
        sprintf(addLog, "Mode: Register, DR: 0x%.1x(%.1u), SR1: 0x%.1x(%.1u), SR2: 0x%.1x(%.1u)\n", DR, DR, SR1, SR1, SR2, SR2);
        exportLog("./instr.log", "a+", addLog);
    #endif
    }

    updateFlag(DR);
}

void LC3VM::AND(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 SR1 = (instr >> 6) & 0x7;
    UINT16 flag = (instr >> 5) & 0x1;

    // Immediate mode
    if (flag) {
        UINT16 imm5 = signExtend(instr & 0x1F, 5);
        regs[DR] = regs[SR1] & imm5;
    #ifdef DEBUG
        char andLog[100];
        sprintf(andLog, "Mode: Immediate, DR: 0x%.1x(%.1u), SR: 0x%.1x(%.1u), imm5: %.2d\n", DR, DR, SR1, SR1, imm5);
        exportLog("./instr.log", "a+", andLog);
    #endif
    }
    // Register mode
    else {
        UINT16 SR2 = instr & 0x7;
        regs[DR] = regs[SR1] & regs[SR2];
    #ifdef DEBUG
        char andLog[100];
        sprintf(andLog, "Mode: Register, DR: 0x%.1x(%.1u), SR1: 0x%.1x(%.1u), SR2: 0x%.1x(%.1u)\n", DR, DR, SR1, SR1, SR2, SR2);
        exportLog("./instr.log", "a+", andLog);
    #endif
    }

    updateFlag(DR);
}

void LC3VM::NOT(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 SR = (instr >> 6) & 0x7;
#ifdef DEBUG
    char notLog[100];
    sprintf(notLog, "DR: 0x%.1x(%.1u), SR: 0x%.1x(%.1u)\n", DR, DR, SR, SR);
    exportLog("./instr.log", "a+", notLog);
#endif

    regs[DR] = ~regs[SR];
    updateFlag(DR);
}

// Conditional JMP
void LC3VM::BR(UINT16 instr) {
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
    UINT16 condFlag = (instr >> 9) & 0x7;
#ifdef DEBUG
    char brLog[100];
    sprintf(brLog, "Conditional JMP (BR) offset: %u, conditional flag : 0x%.3x, Register flag: 0x%.3x\n", pcOffset, condFlag, regs[R_COND]);
    exportLog("./instr.log", "a+", brLog);
#endif

    if (condFlag & regs[R_COND]) {
        // 2021-05-19 sheyifan Bug: caused by integral promotion.
        // Word size of ALU usually equals to word size of int. Different integer
        // will promote to int before calculation. So we need to cast result into
        // unsigned 16 bits.
        // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
        regs[R_PC] += pcOffset;
    }
}

void LC3VM::JMP(UINT16 instr) {
    // RET happens when SR is all zeros
    UINT16 SR = (instr >> 6) & 0x7;
    regs[R_PC] = regs[SR];
#ifdef DEBUG
    char jmpLog[100];
    sprintf(jmpLog, "JMP to: 0x%.2x(%.1u), Address: 0x%.4x(%.5u)\n", SR, SR, regs[SR], regs[SR]);
    exportLog("./instr.log", "a+", jmpLog);
#endif
}

void LC3VM::JSR(UINT16 instr) {
    UINT16 flag = (instr >> 11) & 0x1;
    regs[R_R7] = regs[R_PC];
    // Immediate mode (JSR)
    if (flag) {
        UINT16 pcOffset = signExtend(instr & 0x7FF, 11);
        // 2021-05-19 sheyifan Bug: caused by integral promotion.
        // Word size of ALU usually equals to word size of int. Different integer
        // will promote to int before calculation. So we need to cast result into
        // unsigned 16 bits.
        // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
        regs[R_PC] += pcOffset;
    #ifdef DEBUG
        char jsrLog[100];
        sprintf(jsrLog, "Immediate mode (JSR), JMP offset: 0x%.2x(%.1u), To: 0x%.4x(%.5u)\n", pcOffset, pcOffset, regs[R_PC], regs[R_PC]);
        exportLog("./instr.log", "a+", jsrLog);
    #endif
    }
    // Register mode (JSRR)
    else {
        UINT16 SR = (instr >> 6) & 0x7;
        regs[R_PC] = regs[SR];
    #ifdef DEBUG
        char jsrrLog[100];
        sprintf(jsrrLog, "Register mode (JSR), To: 0x%.4x(%.5u)\n", regs[R_PC], regs[R_PC]);
        exportLog("./instr.log", "a+", jsrrLog);
    #endif
    }
}

void LC3VM::LD(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.This case, formal argument of readMemory() is unsigned
    // 16 bits, which casts the parameter.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    regs[DR] = readMemory(regs[R_PC] + pcOffset);
    updateFlag(DR);
}

void LC3VM::LDI(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    UINT16 pc = regs[R_PC] + pcOffset;
    regs[DR] = readMemory(readMemory(pc));
    updateFlag(DR);
}

void LC3VM::LDR(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 SR = (instr >> 6) & 0x7;
    UINT16 addressOffset = signExtend(instr & 0x3F, 6);
    UINT16 addr = regs[SR] + addressOffset;
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    regs[DR] = readMemory(addr);
    updateFlag(DR);
}

void LC3VM::LEA(UINT16 instr) {
    UINT16 DR = (instr >> 9) & 0x7;
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
#ifdef DEBUG
    char leaLog[100];
    sprintf(leaLog, "DR: 0x%.1x(%.2u), PC: 0x%.4x(%.5u), offset: 0x%.4x(%.5u)", DR, DR, regs[R_PC], regs[R_PC], pcOffset, pcOffset);
    this->exportLog("./instr.log", "a+", leaLog);
#endif
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    regs[DR] = regs[R_PC] + pcOffset;
    updateFlag(DR);
}

void LC3VM::ST(UINT16 instr) {
    UINT16 SR = (instr >> 9) & 0x7;
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
    UINT16 pc = regs[R_PC] + pcOffset;
    memory[pc] = regs[SR];
}

void LC3VM::STI(UINT16 instr) {
    UINT16 SR = (instr >> 9) & 0x7;
    UINT16 pcOffset = signExtend(instr & 0x1FF, 9);
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.This case, formal argument of readMemory() is unsigned
    // 16 bits, which casts the parameter.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    UINT16 pc = regs[R_PC] + pcOffset;
    memory[readMemory(pc)] = regs[SR];
}

void LC3VM::STR(UINT16 instr) {
    UINT16 SR = (instr >> 9) & 0x7;
    UINT16 DR = (instr >> 6) & 0x7;
    UINT16 addressOffset = signExtend(instr & 0x3F, 6);
    // 2021-05-19 sheyifan Bug: caused by integral promotion.
    // Word size of ALU usually equals to word size of int. Different integer
    // will promote to int before calculation. So we need to cast result into
    // unsigned 16 bits.
    // https://zh.wikipedia.org/wiki/%E6%95%B4%E5%9E%8B%E6%8F%90%E5%8D%87
    UINT16 desAddress = regs[DR] + addressOffset;
    memory[desAddress] = regs[SR];
}

void LC3VM::FPUTS() {
    // 2021-05-27 TRAP ROUTINE .PUTS always search for string from bias stored
    // in R0 (TRAP ROUTINES code is generated automatically by assembler)
    UINT16* c = memory + regs[R_R0];
    while (*c) {
        putc((char)*c, stdout);
        c++;
    }
#ifdef DEBUG
    char putsLog[100];
    sprintf(putsLog, "Character start from: 0x%.4x(%.5u) to 0x%.4x(%.5u)\n", regs[R_R0], regs[R_R0], c - memory, c - memory);
    exportLog("./instr.log", "a+", putsLog);
#endif

    fflush(stdout);
}

void LC3VM::FGETC() {
    regs[R_R0] = (UINT16) getchar();
}

void LC3VM::FOUT() {
    putc((char) regs[R_R0], stdout);
    fflush(stdout);
}

void LC3VM::FIN() {
    printf("Please enter a character:\n");
    char c = getchar();
    putc(c, stdout);
    regs[R_R0] = (UINT16) c;
}

void LC3VM::FPUTSP() {
    UINT16* c = memory + regs[R_R0];
    while(*c) {
        char char1 = (*c) & 0xFF;
        putc(char1, stdout);
        char char2 = (*c) >> 8;
        if (char2) {
            putc(char2, stdout);
        }
        c++;
    }

    fflush(stdout);
}

void LC3VM::FHALT() {
    puts("HALT!");
    fflush(stdout);
    running = false;
}
